﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;

namespace Player
{
    public class WriteLock : IDisposable
    {
        protected ReaderWriterLock m_rwl;

        public WriteLock(ReaderWriterLock rwl, int timeout)
        {
            m_rwl = rwl;

            // This collision is only caused by a coding
            // failure.  It can be removed in release builds
#if DEBUG
            if (m_rwl.IsReaderLockHeld)
            {
                throw new ApplicationException("read write lock collision");
            }
#endif

            m_rwl.AcquireWriterLock(timeout);
        }

        ~WriteLock()
        {
            Debug.Assert(false, "Error: WriteLock is used incorrectly.\nUsage: using (WriteLock)");
        }

        #region IDisposable Members

        public void Dispose()
        {
            m_rwl.ReleaseWriterLock();

            // Suppress GC calling destructor.
            GC.SuppressFinalize(this);
        }

        #endregion
    }

    public class ReadLock : IDisposable
    {
        protected ReaderWriterLock m_rwl;

        public ReadLock(ReaderWriterLock rwl, int timeout)
        {
            m_rwl = rwl;
            m_rwl.AcquireReaderLock(timeout);
        }

        ~ReadLock()
        {
            Debug.Assert(false, "Error: ReadLock is used incorrectly.\nUsage: using (ReadLock)");
        }

        #region IDisposable Members

        public void Dispose()
        {
            m_rwl.ReleaseReaderLock();

            // Suppress GC calling destructor.
            GC.SuppressFinalize(this);
        }

        #endregion
    }

    public class WorkerThreadData : IDisposable
    {
        protected ReaderWriterLock m_rwl = new ReaderWriterLock();

        protected int m_timeout = 5000;

        public WorkerThreadData()
        {
        }

        public WriteLock WriteLock
        {
            get
            {
                return new WriteLock(m_rwl, m_timeout);
            }
        }

        public ReadLock ReadLock
        {
            get
            {
                return new ReadLock(m_rwl, m_timeout);
            }
        }

        protected bool m_disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!m_disposed)
            {
                if (disposing)
                {
                    m_rwl = null;
                }
                m_disposed = true;
            }
        }

        ~WorkerThreadData()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    /// <summary>
    /// A thread-safe buffer to store frame data in limited space
    /// If space is not enough, the writer thread will be blocked
    /// If the data is not ready yet, the reader thread will be blocked
    /// </summary>
    public class FrameBuffer : WorkerThreadData
    {
        public event EventHandler<FrameBufferUpdatedEventArgs> Updated;

        // A flag used to block reader thread,Seek or write funciton will give single to allow it continues
        private ManualResetEvent m_dataReady_Read = new ManualResetEvent(false);

        // A flag used to block writer thread
        private ManualResetEvent m_bufferAvailable_Write = new ManualResetEvent(true);

        private LinkedListNode<FrameBase> m_current;
        private LinkedList<FrameBase> m_items = new LinkedList<FrameBase>();

        private int m_size;
        public int Size
        {
            get { return m_size; }
            set { m_size = value; }
        }

        public int BufferSize
        {
            get;
            private set;
        }

        public FrameBuffer(int size)
        {
            BufferSize = 0;
            m_timeout = -1;
            m_size = size;
        }

        public void StopThread()
        {
            m_bufferAvailable_Write.Set();
        }

        /// <summary>
        /// Called by writer thread  
        /// </summary>
        /// <param name="item"></param>
        public void Write(Frame item)
        {
            // Make sure we have enough space in the buffer
            using (ReadLock)
            {
                if (m_items.Count >= m_size)
                {
                    m_bufferAvailable_Write.Reset();
                }
            }

            // Wait if we don't have enough space
            m_bufferAvailable_Write.WaitOne();

            using (WriteLock)
            {
                m_items.AddLast(item);
                BufferSize += item.FrameSize;
                m_dataReady_Read.Set();//写了，可以读
            }

            OnUpdate();
        }

        /// <summary>
        /// Find if the buffer contains any frame with the specified timestamp
        /// It should be called by reader thread only
        /// </summary>
        /// <param name="timestamp"></param>
        /// <returns></returns>
        public bool Seek(long timestamp)
        {
            bool result = false;
            // It will update the m_current pointer.  Thus, we need to use WriteLock
            using (WriteLock)
            {
                if (m_items.Count != 0 &&
                    timestamp >= m_items.First.Value.TimeStamp &&
                    timestamp <= m_items.Last.Value.TimeStamp)
                {
                    m_current = null;
                    LinkedListNode<FrameBase> current = m_items.First;
                    while (timestamp > current.Value.TimeStamp &&
                           current.Next != null)
                    {
                        if (current.Next.Value.Type == 0 &&
                            current.Next.Value.TimeStamp < timestamp)
                        {
                            m_current = current;
                        }
                        current = current.Next;
                        Thread.Sleep(0); // Yield to other threads
                    }
                    result = true;
                    m_dataReady_Read.Set();
                }
            }

            return result;
        }

        public IEnumerable<FrameBase> GetFrames(long timestamp)
        {
            IList<FrameBase> frames = new List<FrameBase>();
            using (ReadLock)
            {
                if (m_items.Count != 0 &&
                    timestamp >= m_items.First.Value.TimeStamp &&
                    timestamp <= m_items.Last.Value.TimeStamp)
                {
                    LinkedListNode<FrameBase> start = m_items.First;
                    LinkedListNode<FrameBase> current = m_items.First;
                    while (current != null && timestamp > current.Value.TimeStamp)
                    {
                        if (current.Value.Type == 0 && timestamp > current.Value.TimeStamp)
                        {
                            start = current;
                        }
                        current = current.Next;
                        Thread.Sleep(0); // Yield to other threads
                    }

                    current = start;
                    while (current != null && timestamp > current.Value.TimeStamp)
                    {
                        frames.Add(current.Value);
                        current = current.Next;
                        Thread.Sleep(0); // Yield to other threads
                    }
                }
            }
            return frames;
        }

        /// <summary>
        /// Reset the frame buffer.  Throw everything away.
        /// It should be called by reader thread only
        /// </summary>
        public void Reset()
        {
            using (WriteLock)
            {
                m_current = null;
                m_items.Clear();
                BufferSize = 0;
                m_dataReady_Read.Reset();
                m_bufferAvailable_Write.Set();
            }
            OnUpdate();
        }

        public FrameBase Read()
        {
            // If there is no data, this will block
            m_dataReady_Read.WaitOne();

            using (WriteLock)
            {
                if (m_dataReady_Read.WaitOne(0))
                {
                    if (m_current == null) m_current = m_items.First;
                    else m_current = m_current.Next;

                    Debug.Assert(m_current != null);
                    if (m_current.Next == null) m_dataReady_Read.Reset();
                    return m_current.Value;
                }
                else
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// It's the only function that allow reader thread to free up some space
        /// This function determine how much data we like to preserve, with reference to the specified frame
        /// </summary>
        /// <param name="frame"></param>
        public void AdjustBufferStart(FrameBase frame)
        {
            FrameBase ret = null;
            using (WriteLock)
            {
                // Preserve at least half of the buffer for user to jump backward
                if (m_items.Count == 0) return;
                LinkedListNode<FrameBase> current = m_items.Find(frame);

                // It may be a fake frame.  It does not exist in buffer but we need it to display time.
                if (current != null)
                {
                    LinkedListNode<FrameBase> bufferStart = null;
                    for (int i = 0; i <= m_size / 2; i++)
                    {
                        current = current.Previous;
                        // Find the earliest i-Frame
                        if (current == null) break;
                        if (current.Value.Type == 0) bufferStart = current;
                    }

                    if (bufferStart != null)
                    {
                        while (m_items.First != bufferStart)
                        {
                            BufferSize -= m_items.First.Value.FrameSize;
                            m_items.RemoveFirst();
                            Thread.Sleep(0); // Yield to other threads
                        }
                    }

                    if (m_items.Count < m_size)
                    {
                        m_bufferAvailable_Write.Set();
                    }
                }
                ret = m_items.First.Value;
            }

            OnUpdate();
        }

        void OnUpdate()
        {
            if (Updated != null)
            {
                FrameBase startFrame = (m_items.First != null) ? m_items.First.Value : null;
                FrameBase endFrame = (m_items.Last != null) ? m_items.Last.Value : null;

                Updated(this, new FrameBufferUpdatedEventArgs(startFrame, endFrame));
            }
        }
    }

    public class FrameBufferUpdatedEventArgs : EventArgs
    {
        public FrameBufferUpdatedEventArgs(FrameBase startFrame, FrameBase endFrame)
        {
            StartFrame = startFrame;
            EndFrame = endFrame;
        }

        public FrameBase StartFrame { get; protected set; }
        public FrameBase EndFrame { get; protected set; }
    }
}
